-
-
Notifications
You must be signed in to change notification settings - Fork 4.6k
fix: ensure derived effects are recreated when needed #16595
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
|
Potential fix for #16594 |
6d94df6
to
e2a1d9f
Compare
e2a1d9f
to
ee2e386
Compare
@@ -671,6 +672,10 @@ export function get(signal) { | |||
|
|||
if (is_dirty(derived)) { | |||
update_derived(derived); | |||
} else if ((derived.f & HAS_EFFECTS) !== 0 && (derived.effects === null || derived.effects.length === 0)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new test can be verified by adding && false
to the end of this if statement and running pnpm test
.
ee2e386
to
d4cb4e4
Compare
|
Playground demonstrating fix: https://svelte.dev/playground/719a2a834fd34d02a204ac4b7c65973b?version=pr-16595 |
@Rich-Harris is there any chance you could have a look at this? It's a small but important detail in the reactivity system. |
d4cb4e4
to
a2329e0
Compare
Adds HAS_EFFECTS flag to track when deriveds contain side effects and re-runs computation when effects are missing after reconnection.
a2329e0
to
942228e
Compare
Solution looks sound from a first look, though can you make it so that the |
@dummdidumm thanks, I'll have a look at using |
I'm a bit out of depth here, but based on your suggestion this is the best I could come up with for setting the effects to a special value: gitbutlerapp@75d3381 Having the effects be of type |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah that commit was what I had in mind but after seeing it I agree that a flag is better (we can always go that route if we reach the magic limit of 32 flags and need room). cc @Rich-Harris
Could you add a changeset? Then we're good to go from my side.
There's a subtle question around expected behaviour here: is it right that effects are destroyed when nothing is listening to a derived? <script>
class Timer {
constructor(text) {
this.elapsed = $state(0);
this.text = $derived(text + ': ' + this.elapsed);
$effect(() => {
const interval = setInterval(() => {
this.elapsed += 1;
}, 1000);
return () => clearInterval(interval);
});
}
}
let a = $state('hello');
let b = $derived(new Timer(a));
let visible = $state(true);
</script>
<input bind:value={a} />
<button onclick={() => visible = !visible}>toggle</button>
{#if visible}
<p>{b.text}</p>
{/if} If With this PR, the effect is recreated when To me it feels like the value of diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js
index 22a1890e0f..363946f292 100644
--- a/packages/svelte/src/internal/client/runtime.js
+++ b/packages/svelte/src/internal/client/runtime.js
@@ -403,6 +403,7 @@ function remove_reaction(signal, dependency) {
if (
reactions === null &&
(dependency.f & DERIVED) !== 0 &&
+ /** @type {Derived} */ (dependency).effects === null &&
// Destroying a child effect while updating a parent effect can cause a dependency to appear
// to be unused, when in fact it is used by the currently-updating parent. Checking `new_deps`
// allows us to skip the expensive work of disconnecting and immediately reconnecting it A reasonable person could say that as soon as you start using effects in that manner, you're colouring so far outside the lines that you shouldn't have any expectations of the behaviour. I'm not sure. Thoughts? |
Problem
Effects created inside
$derived
statements are not re-executed when the derived is accessed after being disconnected and reconnected to the reactive dependency graph.Reproduction scenario:
const result = $derived(/* contains $effect */)
This happens because when a derived is disconnected from the graph (no more reactions), its effects are destroyed via
destroy_derived_effects()
. When the derived is later reconnected, the effects are not recreated since the derived appears "clean" and doesn't need re-execution.works here: https://svelte.dev/playground/719a2a834fd34d02a204ac4b7c65973b?version=5.35.3
does not work: https://svelte.dev/playground/719a2a834fd34d02a204ac4b7c65973b?version=5.35.4
Solution
Introduce a
HAS_EFFECTS
flag that tracks when a derived has contained effects, even after they've been destroyed. During reconnection, if a derived has theHAS_EFFECTS
flag but no current effects, we force re-execution to recreate them.Changes:
HAS_EFFECTS = 1 << 24
constanteffects.js
when effects are added to derivedsruntime.js
and trigger re-execution when neededImplementation
Testing
Added a comprehensive test that:
Without fix: 0 effect executions (effects lost forever)
With fix: 1 effect execution (effects recreated on reconnection)